using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using UnityEditor;
using UnityEngine;

namespace ReactUnity.Editor.Generators
{
    public class RuntimeT4Generator
    {
        /// <summary>
        ///     Additional data that can be passed to the templates. First key is the template name, value is Session
        ///     data that is accessible from the template.
        /// </summary>
        public static Dictionary<string, Dictionary<string, object>> Sessions { get; } = new Dictionary<string, Dictionary<string, object>>();

        static List<string> TemplateNames { get; } = new List<string>()
        {
            "ReactActionGenerator"
        };

        /// <summary>
        ///     Generates the code from all templates in the project and saves them to Runtime/Generated folder
        /// </summary>
#if REACT_UNITY_DEVELOPER
        [MenuItem("React/Generate Code From T4 Templates")]
#endif
        public static void GenerateFromTemplates()
        {
            foreach (var templateName in TemplateNames)
            {
                var uniqueTemplates = new HashSet<string>();
                var templates = typeof(RuntimeT4Generator).Assembly.GetTypes()
                    .Where(type => type.FullName.Contains(templateName));
                foreach (Type template in templates)
                {
                    // Get the template name
                    var name = GetTemplateName(template);

                    // Ignore duplicates and templates that are not root templates
                    if (uniqueTemplates.Contains(name)) continue;
                    if (!template.BaseType.Name.Equals(templateName + "Base")) continue;
                    uniqueTemplates.Add(name);

                    // Add common data to the session
                    var session = Sessions.ContainsKey(name) ? Sessions[name] : new Dictionary<string, object>();
                    AddCommonSessionValues(ref session);

                    // Using reflection, create an instance of the template and run it
                    var transformation = Activator.CreateInstance(template);
                    var transformTextMethod = template.GetMethod("TransformText");
                    var sessionsProperty = template.GetProperty("Session");
                    var errorsProperty = template.GetProperty("Errors");

                    // Set session
                    sessionsProperty.SetValue(transformation, session);

                    // Transform text
                    var content = (string) transformTextMethod.Invoke(transformation, null);

                    // Write to file in Runtime/Generated folder
                    WriteGenerationResult(name, content);

                    // Get errors
                    var errors = (CollectionBase) errorsProperty.GetValue(transformation);
                    if (errors.Count > 0)
                    {
                        Debug.LogError("An error occured while generating code from template " + name);
                        foreach (var error in errors)
                        {
                            Debug.LogError("Error in template " + name + ": " + error);
                        }
                        return;
                    }
                }
            }

            // Refresh the project to show the generated files
            AssetDatabase.Refresh();
        }

        /// <summary>
        ///     Gets the name of the template, by getting last part of the namespace.
        /// </summary>
        /// <param name="template"></param>
        /// <returns></returns>
        private static string GetTemplateName(Type template)
        {
            var name = template.Namespace;
            var index = name.LastIndexOf('.');
            return name.Substring(index + 1);
        }

        /// <summary>
        ///     Creates a path to the generated file, and creates the directory if it doesn't exist. Writes the content to the file.
        /// </summary>
        /// <param name="name">Name of the file</param>
        /// <param name="content">Content of the file</param>
        /// <param name="sourceFilePath">Autogenerated path of the source file</param>
        private static void WriteGenerationResult(string name, string content, [CallerFilePath] string sourceFilePath = "")
        {
            // Get location of this cs file
            var path = Path.GetDirectoryName(sourceFilePath);
            // Go back from Editor to Runtime and create a Generated folder
            var outputPath = Path.Combine(path, "..", "..", "Runtime", "Generated", name + ".cs");
            Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
            File.WriteAllText(outputPath, content);
        }

        /// <summary>
        ///     Adds common data to the session
        /// </summary>
        /// <param name="session">Session data to modify</param>
        private static void AddCommonSessionValues(ref Dictionary<string, object> session)
        {
            session["Common"] = new GeneratorHelpers();
        }
    }
}
